Skip to content

feat(spec): declare IMetadataService.loadManyKeyed beside its plural-read siblings - #19609

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-15385-metadata-service-loadmanykeyed
Sep 22, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-15385-metadata-service-loadmanykeyed

Conversation

@os-warren

@os-warren os-warren commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15385

Declares loadManyKeyed? on IMetadataService, deletes the local structural type the ObjectQL governance audit used to reach it, and documents the member.

Execution of the recorded ruling — director seat, decision batch #123 item 5, 2026-09-12, comment 5644711080, maintainer verbatim 「同意」. That ruling picks option 1 of the two the card put to triage and enumerates four items; all four are below. Option 2 (leave it undeclared on purpose) is ruled out and is not re-opened here.

Clause-②: yes

The declaration

packages/spec/src/contracts/metadata-service.ts — the new member sits immediately after its unkeyed twin loadMany? (line 675 on the base commit), inside the same IMetadataService declaration that already carries loadMany? and loadDiagnosed?.

⚠️ The signature is spelled in words here on purpose: this surface deletes tag-shaped tokens, generics included, from prose and from code fences alike, so a literal copy of it would arrive mutilated. loadManyKeyed? is optional, is generic in one parameter T that defaults to unknown, takes type: string plus an optional options bag typed as a Record from string to unknown, and resolves to an Array of { name: string; data: T } pairs. That is the signature the ruling names, member for member. The diff is the authority on it, not this paragraph.

It is optional, like both siblings, so every existing IMetadataService implementation still satisfies the contract unchanged and the typeof ... === 'function' probe stays the way a caller asks for it.

Why an inline pair shape rather than the published MetadataKeyedItem

The ruling writes the return type inline, and that is also the only spelling available. MetadataKeyedItem is declared in packages/metadata/src/loaders/loader-interface.ts and exported from @objectstack/metadata, which depends on @objectstack/spec; packages/spec declares no workspace dependency at all (pg-connection-string and zod). Importing the named type here would invert that edge and close a cycle. The inline pair is also the file's own precedent: loadDiagnosed? declares its result inline in exactly the same way.

The two shapes are structurally identical — MetadataKeyedItem is readonly name: string beside readonly data: T, and a readonly property is assignable to a mutable one — so MetadataManager implements IMetadataService keeps compiling with no edit to packages/metadata, which is what the green build below shows.

The four ruled items

  1. Declared — as above, with a docblock citing this ruling the way loadDiagnosed's cites #4127 batch 4.
  2. Local structural type deletedpackages/objectql/src/plugin.ts loses KeyedPluralMetadataRead (the type and its docblock), and the three service lookups in resolveGovernanceMetadataService now ask for IMetadataService alone instead of intersecting it. Occurrences of that type name under packages/ go 4 to 0 — 1 declaration plus 3 use sites; an earlier draft of this line said 3, corrected against the blob by the at-tier review (lit control: IMetadataService in the same file = 13, so the zero is a reading). check:slot-lookup stays green — see below.
  3. Docscontent/docs/kernel/contracts/metadata-service.mdx gains the member, in the interface excerpt's Loader reads (optional) group and as a new loadManyKeyed subsection. Section choice and a contrary fact about it are in the acceptance notes.
  4. CarriersClause-②: yes above; minor changeset (@objectstack/spec, whose changesets fixed group already carries @objectstack/objectql). The needs:contract-review carrier was not hung by this branch — the dev never wrote a label. ⚠️ Corrected provenance: the owning seat hung it on 2026-09-21T16:58:44Z, after the dev's push, which the PR's own event log records; a reader checking the labels today will find it present. ⛔ The earlier "see the acceptance notes" pointer is dropped: those notes never mentioned the carrier.

Round 2 — head b96baa08f2 (2 files, +38 / −13)

Three corrections, all prose; the PR's file list is unchanged at 5 and no new path was pulled in.

  1. The dangling citation. check:issue-citations was RED at 97a639c5b3: the new docblock cited an issue that returns 404 (LIT CONTROL: its neighbour #14424 → 200, so the 404 is a reading). ⭐ The replacement was not guessed#15378 was verified four ways before being named: HTTP 200, merged: true, merged_at, base main, and the depth-immune one — origin/main holds the implementation it added (git grep 'async loadManyKeyed' origin/main -- packages/metadata/src/metadata-manager.ts = 1; nonsense control = 0).
    ⚠️ The gate's own remedy arm two does not work, and this is filed as finding(pm): check-issue-citations prints a remedy arm its own grammar cannot honour — 「keep the number and say in prose」 still fails, because CITATION_RE sees the # #19614: keeping the # still matches CITATION_RE, and NON_CITATION_HEADS excuses only ordinal heads — there is no prose-acknowledgement mechanism in the script. So the dead card is kept as bare digits without a leading hash, with a sentence saying why. Greppable, and nothing dangles.
  2. The docs example taught what its own Callout rejects. It probed nothing and null-coalesced to [], turning absence into an empty set — on the one member whose reason for existing is that silent drops are dangerous. It now reads the member into a local, guards on typeof === 'function', and its else branch says why absence is not emptiness, matching plugin.ts. ?? [] is gone from the page (0 hits).
  3. A name that named nothing. "customization container" had 0 hits on origin/main across packages/, content/ and docs/. The right vocabulary came from MetadataKeyedItem's own docblock: an aggregated defineView container "has no own name BY DESIGN (its identity is the target object)". Both carriers now say that, each with an explicit disclaimer that it is not the ADR-0005 sys_metadata org customization overlay — which this same page documents separately.

Gate readings at b96baa08f2⚠️ check:issue-citations is recorded here and ⛔ not in the derived-families row (which is below, in the Local runs table — an earlier draft of this line said "above"), because its root script is --self-test only while CI runs the self-test and the scan; reporting the alias as a pass is what produced the red in the first place. Run as the SCAN: node scripts/check-issue-citations.mjs --base origin/mainEXIT=0, captured before any pipe, re-run at the final head → EXIT=0 (4 citations judged across 13 files; 2 resolves, 2 resolves-as-pull-request).
pnpm lint whole repo EXIT=0 · spec build success · check:generated all 15 up to date against a fresh build · spec typecheck pass · spec test 509 files / 14901 passed · objectql typecheck EXIT=0 · check:slot-lookup holds · check:nul-bytes OK plus a hand control-character scan of both edited files.

Mechanical proof the .ts edit is docblock-only: every added and removed line in git diff 97a639c5b3..b96baa08f2 -- packages/spec/src/contracts/metadata-service.ts is a comment line — zero non-comment lines. No type or runtime surface moved, so the ablation recorded at 97a639c5b3 still stands and was not re-run.

⚠️ Two prerequisite failures, resolved rather than reported as passes, both artefacts of a fresh worktree and neither about the diff: check:docs-transcript-drift exit 3 (@objectstack/lint unbuilt) → built, re-ran, EXIT=0; objectql typecheck first exit 2 with 42 errors, all TS2307 Cannot find module from an unbuilt dependency closure → built, re-ran, EXIT=0.

⚠️ A negative reading deliberately NOT relied on: git merge-base --is-ancestor on #15378's squash commit exited 1, but this checkout is shallow and the control leg was a shallow-window near-relative — so that negative is void, not evidence. The tree read and the API's merged / merged_at answer the question without a history walk.

Verification

Reverse verification, because this is a cross-package type change and a green typecheck against a stale .d.ts is indistinguishable from a real one. Run from the committed state through scripts/ablation-replace.mjs, with the on-disk and in-dist evidence the tool produces:

  • Mutate — the declared member renamed at its anchor. Anchor hits 1 to 0, blob bd37483b1715 to 3a9e85d219ad.
  • Reached the artifactscripts/ablation-dist-preflight.mjs found the mutated marker in 2 built files (packages/spec/dist/contracts/index.d.ts and .d.mts), so the run below read the rebuilt declarations and not a cache.
  • The ablation runtsc --noEmit in packages/objectql went red with exactly one error, and it is the call site: src/plugin.ts(2593,35): error TS2339: Property 'loadManyKeyed' does not exist on type 'IMetadataService'.
  • Restore — blob back to bd37483b1715, equal to HEAD, git diff HEAD empty, whole-tree git status --porcelain empty. After a rebuild the mutated marker is gone from dist (0 occurrences) and the real member is back (2), and tsc --noEmit in packages/objectql is green with zero output.

That is the proof for ruled item 2: the call site now reads the contract, and it reads only the contract.

Local runs. ⚠️ Provenance corrected — this table is not all from one head. The nine readings restated in the Round 2 section were taken at the final head b96baa08f2; every other row here — the objectql typecheck, the changeset gates, the 14 docs gates and the 20 further derived families — was measured at 97a639c5b3, before round 2 rewrote the .mdx. ⛔ Nothing is actually unmeasured at the final head: CI ran the whole docs family green there, including "packages/spec/src/** doc-block symbol anchors resolve". It is the sentence that over-claimed its own provenance, not the work.

check result
pnpm lint (whole repo, eslint . --no-inline-config) 0 — clean
pnpm --filter @objectstack/spec build success; 34/34 declaration files emitted
pnpm --filter @objectstack/spec check:generated All 15 generated artifacts up to date
pnpm --filter @objectstack/spec typecheck pass (includes check:test-typecheck)
pnpm --filter @objectstack/spec test 509 files, 14901 passed, 1 todo
pnpm --filter @objectstack/objectql typecheck pass
pnpm --filter @objectstack/objectql test 303 files, 5050 passed
pnpm check:slot-lookup ✓ holds — 106 unswept sites in 25 files, none new, baseline key set verified against 0e658fb: no files added
pnpm check:nul-bytes ✓ 9156 text files scanned, no raw control bytes
changeset gates (check:empty-changeset, check:adr-0087-registration, check:changeset-no-major, check:changeset-gate-self-tests) pass
docs gates (check:doc-anchors, check:doc-authoring, check:doc-frontmatter, check:docs-section-name, check:docs-single-h1, check:docs-redirects, check:docs-spec-enumerations, check:docs-transcript-drift, check:docs-audit-scope, check:doc-route-spelling, docs-audit/check-affected-docs, docs-audit/check-drift-comment, check:section-landing-index, check:keyed-text-bounds) pass
further derived families run (check:type-check-coverage, check:test-source-alias, check:published-files, check:dts-closure, check:lean-entry-closure, check:cross-package-test-inputs, check:spec-docblock-symbol-anchors, check:comment-mask-adoption, check:comment-mask-corpus, check:undeclared-dep-imports, check:query-options-erasure, check:spec-parsed-alias, check:objectql-double-limit, check:engine-double-contract, check:durability-log-level, check:published-readme-links, check:pm-prior-rulings, check:sourcemap-no-sources-content, check:strictness-ledger, check:skill-refs) pass
check:type-check-debt, check:dual-build-cjs-loads NOT MEASURED — both exited 3 (PREREQUISITE NOT MET); each needs a whole-workspace build this branch did not run. Neither a pass nor a finding. Declared to CI.

What the generators actually moved: nothing

Measured rather than inferred, and this was the one prediction worth testing. Six generators were run against the built tree — gen:api-surface, gen:export-origins, gen:spec-changes, gen:schema, gen:docs, gen:declaration-map — each exiting 0, after which git status --porcelain listed no generated artefact. check:generated independently reports all 15 up to date. So an optional member on a published interface moves none of the four artefacts that name IMetadataService, exactly as the claim predicted.

⚠️ One reading on the way there was not a finding and should not be read as one: check:api-surface first reported stale with PREREQUISITE NOT MET — this gate reads built output, and what is on disk predates the sources. The dist had been built before a later edit to the test file, which is a build input. Rebuilding cleared it. It was never an artefact move.

Acceptance notes

⛔ Noted, not filed, and deliberately not fixed here — each is outside this card's ruled four items.

  • The docs page documents loadManyKeyed ahead of its own declared sibling loadMany?. On content/docs/kernel/contracts/metadata-service.mdx, loadMany appeared 0 times before this change (lit control on the same page: loadDiagnosed = 5, so the zero is a reading). The page's interface excerpt is explicitly partial and says so — its line 29 points at IMetadataService in the source for the full member list — so this is a documentation gap rather than a contradiction, but the ordering is odd for a reader and it is being handed to the seat to file as its own card. Widening this PR to also document loadMany was declined on purpose.
  • Section choice, and why. The new member is documented as a loadManyKeyed subsection under Core CRUD, immediately after load / loadDiagnosed and before list / listNames. Bulk Operations was considered and rejected: despite the name, that section on this page documents bulk writes (bulkRegister / bulkUnregister), so a plural loader read filed there would sit in the write section. The chosen spot is the page's loader-read run, one step from the plural registry reads a reader would be comparing it against.
  • The page's own loadDiagnosed example still teaches the shape this PR's new example refuses. At content/docs/kernel/contracts/metadata-service.mdx:138, two sections above the new probe-first example, the pre-existing loadDiagnosed snippet spells an optional call plus ?? {} — absence collapsing into a value, which is exactly what the new example's else branch says not to do ("Do NOT fall through to an empty set") and what the info Callout restates ("never as an empty set"). It is present at this PR's merge base and untouched here (?? [] on this page at head: 0, git grep exit 1 captured before any pipe; lit control ?? {} on the same page: 1, at :138, so the zero is a reading). The page is now internally inconsistent in style rather than wrong. ⛔ Recorded here rather than filed as a card, per the standing rule: the question "which PR will touch this file?" has an answer, and it is this one — so the note belongs where the next editor of the page will read it. Widening this PR to rewrite a snippet outside its four ruled items was declined on purpose.
  • The #16090 serialisation caveat recorded in the ruling's item 3 is spent. That issue is closed, and no open pull request holds the page. Nothing was serialised against and nothing waited.

Landing

⛔ Draft on purpose, and it stays that way from this branch. No flip to ready, no enqueue, no auto-merge. Landing is the owning seat's act after an at-tier contract review.


Generated by Claude Code

…read siblings

The keyed plural loader read shipped as a public member on MetadataManager with
no declaration on the contract its two siblings loadMany? and loadDiagnosed? are
declared on, so the one cross-package caller — the ObjectQL governance audit —
narrowed the service slot with a local structural type written beside the call
site. Declare the member on IMetadataService, delete the local type, and let the
call site read the contract.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
A double holding one fixed body cannot satisfy a caller-chosen `T` — the three
object-literal implementations were concrete and `check:test-typecheck` refused
them (TS2322, a signature the shrink-only ledger does not record). Declare each
double `<T = unknown>` and hand the body back under it.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 21, 2026
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 5 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via IMetadataService (symbol, a top-level interface))
  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/contracts/cache-service.mdx (via IMetadataService (symbol, a top-level interface))
  • content/docs/kernel/contracts/index.mdx (via IMetadataService (symbol, a top-level interface))
  • content/docs/kernel/contracts/metadata-service.mdx (via IMetadataService (symbol, a top-level interface), loadManyKeyed (symbol, a method of interface IMetadataService; a method of type KeyedPluralMetadataRead))
  • content/docs/kernel/index.mdx (via IMetadataService (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8packageMentionDocs.

Which tree this was computed on

This run read content/docs from 216cc3273f3c24c061ee4b87084f52532fdeb14a — the merge of head b96baa08f273ba99c0986c24f706b693c2061b6a into base 5c5b67fc4140f76ca3158acea9e0845d9eebfad8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 216cc3273f3c24c061ee4b87084f52532fdeb14a && git checkout 216cc3273f3c24c061ee4b87084f52532fdeb14a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 b96baa08f273ba99c0986c24f706b693c2061b6a && git checkout -B drift-repro 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 && git merge --no-ff b96baa08f273ba99c0986c24f706b693c2061b6a

node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…probe-first example

Three corrections to the loadManyKeyed declaration and its page.

The docblock cited the card that ordered the implementation, which has since
been deleted from the board and no longer resolves; check:issue-citations
refused it as allocated-but-absent. Cite the merged pull request that is the
live record instead, and keep the deleted card's number in prose, without a
leading hash, so it stays greppable without minting a reference that dangles.

"a customization container" named nothing in the tree and collided with the
ADR-0005 sys_metadata overlay, which is a different mechanism this very page
documents. The real case is the aggregated defineView container, which the
MetadataKeyedItem docblock already names; both carriers now say so.

The page's example did an optional call and null-coalesced to an empty array,
turning an absent member into an empty set — the opposite of what the callout
beneath it says and of what the one real caller does. It now probes first and
spells out why absence is not emptiness.

Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Ready, green, reviewed — and the last step is blocked with no channel

All three landing preconditions are met and recorded on the card:

  • ① at-tier contract review: PASS, with the served tier measured from the reviewer's transcript per assistant row, and the tier constant re-read from origin/main at 2026-09-21T18:06Z before the round was dispatched.
  • check-clause2-carriers --pair: EXIT=0, exit code captured before any pipe.
  • ③ CI by job conclusion, latest run per check NAME: 35 distinct names, 0 failure, 0 cancelled; every skip is on the EXPECTED_SKIPS roster. ⛔ No aggregate roll-up was read as the verdict.
  • Governance: ungoverned, 0 of 6 — predicate executed (GOVERNED_SURFACES + governedPathsIn imported from origin/main) rather than recalled, with a lit control (6/6) and a near-miss control (0/8). ⛔ .github/CODEOWNERS was not consulted; it is not a governed surface.

This PR has been flipped draft → ready, confirmed by GET /pulls/{n} returning draft: false — ⛔ not by the POST's status code.

auto_merge could NOT be enabled. The seat's session permission classifier refused the call, and there is no second channel: the MCP enable_pr_auto_merge tool is on this session's deny roster, and ⛔ working around a classifier refusal is not a channel. Recorded rather than retried, per the standing rule.

The seat will not merge this PR, enqueue it by hand, or submit an approving review on it. The only remaining act is enabling auto-merge with merge_method: SQUASH so the merge queue lands it — ⛔ never a direct merge, never a queue bypass.

Action needed from the maintainer or a seat with the channel: enable auto-merge (SQUASH). Everything else here is finished.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

✅ Correction — this PR is NOT blocked any more. It is in the merge queue.

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T00:3xZ.

The maintainer re-granted the permission and the call was retried. ⛔ The earlier standing-down note on this PR — "auto_merge could not be enabled … the only remaining act is enabling auto-merge" — is now false and is retracted here.

⚠️ And the seat's first read-back of that retry was WRONG. Recording it, because it is the more useful half.

PUT .../ccr/auto_merge returned 200 {"enabled":true,"merge_method":"squash"}. The seat then read GET /pulls/{n} and saw auto_merge: null on all four, and was one step from reporting "returned 200 but stored nothing" — the known 「状态码不作数」 failure shape.

That reading was the wrong instrument. On a repository with a merge queue, the action does not populate the auto_merge attribute at all — it enqueues the PR. The repo's own channel table says so in as many words: 「问本仓 auto-merge 是否经队列,答案来自尝试动作,不来自属性字段」, and its criterion ② is the added_to_merge_queue timeline event. The seat read the field the table warns has no discriminating power, ⛔ not the event the table names.

The evidence, on two independent instruments:

  1. Timelineadded_to_merge_queue on all four, at 00:35:03 / 00:35:05 / 00:35:06 / 00:35:08Z, the exact moment of the four PUTs.
  2. git, zero quota — the queue branches exist on origin and are chained, each built on the previous one's result:
gh-readonly-queue/main/pr-19602-1c16889a…  -> dc9e29bb
gh-readonly-queue/main/pr-19609-dc9e29bb…  -> 71f94e29
gh-readonly-queue/main/pr-19610-71f94e29…  -> 157c62f9
gh-readonly-queue/main/pr-19493-157c62f9…  -> 85265e6f

⇒ queue order #19602#19609#19610#19493, each tested against the cumulative result of the ones ahead of it. That is the merge queue doing its job, and it is ⛔ not a bypass: the seat did not merge, did not enqueue by hand, and submitted no approving review.

What happens next

Each PR merges as its queue branch goes green. ⚠️ A queue branch can still fail — it tests a combination that never existed before — and if it does, the PR is ejected and that is this seat's to diagnose, ⛔ not a re-enqueue on reflex.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Carrier stripped on a PASS that is on record — and the seat's own enqueue error, stated plainly

domain:spec execution seat 2, session session_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T00:5xZ.

⛔ What I got wrong

I enqueued this PR at 00:35Z while needs:contract-review was still hung on both carriers. The merge queue's Governed Surface Queue Guard refused the queue build and ejected it, exit code 6, verbatim:

#19609 — ⛔ CARRIES \needs:contract-review` — this pull request may not be in the queue.`

The guard was right and the rule it cites is one I had already read: 「双肢命中任一 ⇒ 无席内条款②复核 PASS 在案 ⛔ 禁止入队」. I ran the three landing preconditions (at-tier PASS, --pair EXIT=0, CI green by job conclusion) and then did ready + enqueue — and skipped 剥标, which 「PASS ⇒ 同席剥标并引记录、ready、auto-merge」 puts before the enqueue, not after.

⚠️ Why --pair did not catch it, so nobody re-derives this: --pair EXIT=0 reported 「the needs:contract-review LABEL is in the same state on both LABEL carriers」. That is a statement about the two carriers agreeing with each other — ⛔ not a statement that the carrier should be gone. I read a consistency row as a release. Two distinct questions, one of which no gate was asking.

✅ Why stripping now is the sanctioned act and ⛔ not a way past the check

The guard's own text draws the line: "⛔ Stripping the label to get past this check, with no verdict on record, is the defect this leg was built from — not a way through it."

There is a verdict on record, and it is cited here rather than asserted:

At-tier contract review, VERDICT: PASS — comment 5765308504 on card #15385, head b96baa08f273ba99c0986c24f706b693c2061b6a (this PR's current head, unmoved), served tier measured from the reviewer's transcript at 145/145 rows at CONTRACT_REVIEW_TIER. Seat disposition adopting it verbatim: comment 5765407945.

⇒ the condition 「PASS ⇒ 同席剥标并引记录」 is satisfied. The carrier is stripped from both carriers — this PR and its card — by the same seat that adopted the verdict, in the four-step label write, with read-back.

What happens next

Carrier stripped on both sides → --pair re-run → re-enqueued. ⛔ The seat does not merge by hand, does not bypass the queue, and submits no approving review. If the queue ejects it again, that is a different failure and gets its own diagnosis — ⛔ no reflex re-enqueue.


Generated by Claude Code

@os-warren
os-warren added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 6175da8 Sep 22, 2026
54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants